// vguest.cpp
//
// A Rivendell switcher driver for the Logitek vGuest Protocol
//
//   (C) Copyright 2002-2005 Fred Gleason <fredg@paravelsystems.com>
//
//      $Id: vguest.cpp,v 1.25 2007/10/04 20:53:39 dmills Exp $
//
//   This program is free software; you can redistribute it and/or modify
//   it under the terms of the GNU General Public License version 2 as
//   published by the Free Software Foundation.
//
//   This program is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU General Public License for more details.
//
//   You should have received a copy of the GNU General Public
//   License along with this program; if not, write to the Free Software
//   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//

#include <stdlib.h>
#include <rddb.h>
#include <globals.h>
#include <vguest.h>


VGuest::VGuest(RDMatrix *matrix,QObject *parent,const char *name)
  : QObject(parent,name)
{
  RDTty *tty;
  QString sql;
  RDSqlQuery *q;
  int n;

  //
  // Get Matrix Parameters
  //
  vguest_matrix=matrix->matrix();
  vguest_porttype=matrix->portType();
  vguest_ipaddress=matrix->ipAddress();
  vguest_username=PadString(matrix->username(),16);
  vguest_password=PadString(matrix->password(),16);
  vguest_ipport=matrix->ipPort();
  vguest_inputs=matrix->inputs();
  vguest_outputs=matrix->outputs();
  vguest_gpis=matrix->gpis();
  vguest_gpos=matrix->gpos();

  //
  // Load Engine Data - Inputs
  //
  sql=QString().sprintf("select NUMBER,ENGINE_NUM,DEVICE_NUM from INPUTS where\
                         (STATION_NAME=\"%s\")&&(MATRIX=%d) order by NUMBER",
			(const char *)matrix->station(),matrix->matrix());
  q=new RDSqlQuery(sql);
  n=1;
  while(q->next()) {
    while(q->value(0).toInt()>n) {
      vguest_input_engine_nums.push_back(-1);
      vguest_input_device_nums.push_back(-1);
      n++;
    }
    vguest_input_engine_nums.push_back(q->value(1).toInt());
    vguest_input_device_nums.push_back(q->value(2).toInt());
    n++;
  }
  delete q;

  //
  // Load Engine Data - Outputs
  //
  sql=
    QString().sprintf("select NUMBER,ENGINE_NUM,DEVICE_NUM from OUTPUTS where\
                       (STATION_NAME=\"%s\")&&(MATRIX=%d) order by NUMBER",
		      (const char *)matrix->station(),matrix->matrix());
  q=new RDSqlQuery(sql);
  n=1;
  while(q->next()) {
    while(q->value(0).toInt()>n) {
      vguest_output_engine_nums.push_back(-1);
      vguest_output_device_nums.push_back(-1);
      n++;
    }
    vguest_output_engine_nums.push_back(q->value(1).toInt());
    vguest_output_device_nums.push_back(q->value(2).toInt());
    n++;
  }
  delete q;

  //
  // Load Engine Data - Relays
  //
  sql=
    QString().sprintf("select NUMBER,ENGINE_NUM,DEVICE_NUM,SURFACE_NUM,\
                       RELAY_NUM from VGUEST_RESOURCES where\
                       (STATION_NAME=\"%s\")&&(MATRIX_NUM=%d)&&\
                       (VGUEST_TYPE=%d) order by NUMBER",
		      (const char *)matrix->station(),matrix->matrix(),
		      RDMatrix::VguestTypeRelay);
  q=new RDSqlQuery(sql);
  n=1;
  while(q->next()) {
    while(q->value(0).toInt()>n) {
      vguest_relays_engine_nums.push_back(-1);
      vguest_relays_device_nums.push_back(-1);
      vguest_relays_surface_nums.push_back(-1);
      vguest_relays_relay_nums.push_back(-1);
      n++;
    }
    vguest_relays_engine_nums.push_back(q->value(1).toInt());
    vguest_relays_device_nums.push_back(q->value(2).toInt());
    vguest_relays_surface_nums.push_back(q->value(3).toInt());
    vguest_relays_relay_nums.push_back(q->value(4).toInt());
    n++;
  }
  delete q;

  //
  // Load Engine Data - Displays
  //
  sql=
    QString().sprintf("select NUMBER,ENGINE_NUM,DEVICE_NUM,SURFACE_NUM\
                       from VGUEST_RESOURCES where\
                       (STATION_NAME=\"%s\")&&(MATRIX_NUM=%d)&&\
                       (VGUEST_TYPE=%d) order by NUMBER",
		      (const char *)matrix->station(),matrix->matrix(),
		      RDMatrix::VguestTypeDisplay);
  q=new RDSqlQuery(sql);
  n=1;
  while(q->next()) {
    while(q->value(0).toInt()>n) {
      vguest_displays_engine_nums.push_back(-1);
      vguest_displays_device_nums.push_back(-1);
      vguest_displays_surface_nums.push_back(-1);
      n++;
    }
    vguest_displays_engine_nums.push_back(q->value(1).toInt());
    vguest_displays_device_nums.push_back(q->value(2).toInt());
    vguest_displays_surface_nums.push_back(q->value(3).toInt());
    n++;
  }
  delete q;

  //
  // Reconnection Timer
  //
  vguest_reconnect_timer=new QTimer(this,"vguest_reconnect_timer");
  connect(vguest_reconnect_timer,SIGNAL(timeout()),this,SLOT(ipConnect()));

  //
  // Initialize the connection
  //
  switch(vguest_porttype) {
      case RDMatrix::TtyPort:
	tty=new RDTty(rdstation->name(),matrix->port());
	vguest_device=new RDTTYDevice();
	if(tty->active()) {
	  vguest_device->setName(tty->port());
	  vguest_device->setSpeed(tty->baudRate());
	  vguest_device->setWordLength(tty->dataBits());
	  vguest_device->setParity(tty->parity());
	  vguest_device->open(IO_Raw|IO_ReadWrite);
	}
	delete tty;

      case RDMatrix::TcpPort:
	vguest_socket=new QSocket(this,"vguest_socket");
	connect(vguest_socket,SIGNAL(connected()),this,SLOT(connectedData()));
	connect(vguest_socket,SIGNAL(connectionClosed()),
		this,SLOT(connectionClosedData()));
	connect(vguest_socket,SIGNAL(readyRead()),
		this,SLOT(readyReadData()));
	connect(vguest_socket,SIGNAL(error(int)),this,SLOT(errorData(int)));
	ipConnect();
	break;
  }
}


void VGuest::processCommand(RDMacro *cmd)
{
  char buffer[VGUEST_MAX_COMMAND_LENGTH];
  char cmd_byte=0;
  QString label;

  switch(cmd->command()) {
      case RDMacro::SD:
	if((cmd->argQuantity()<5)||
	   (cmd->arg(1).toUInt()>vguest_displays_engine_nums.size())) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  LogLine("*** not enough arguments ***");
	  return;
	}
	if((vguest_displays_engine_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_displays_device_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_displays_surface_nums[cmd->arg(1).toInt()-1]<0)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  LogLine("*** invalid hex parameters ***");
	  return;
	}
	label=cmd->rollupArgs(5).left(VGUEST_MAX_TEXT_LENGTH);
	sprintf(buffer,"\x02%c\x5C%c%c%c%c%c%c%c%s",8+label.length(),
		(char)vguest_displays_engine_nums[cmd->arg(1).toInt()-1],
		(char)(vguest_displays_device_nums[cmd->arg(1).toInt()-1]>>8),
		(char)(vguest_displays_device_nums[cmd->arg(1).toInt()-1]&0xFF),
		(char)vguest_displays_surface_nums[cmd->arg(1).toInt()-1],
		(char)(0xFF&cmd->arg(2).toInt()),
		(char)(0xFF&cmd->arg(3).toInt()),
		(char)(0xFF&cmd->arg(4).toInt()),
		(const char *)label);
	SendCommand(buffer,10+label.length());
	break;

      case RDMacro::ST:
	if((cmd->arg(1).toInt()<1)||(cmd->arg(1).toInt()>vguest_inputs)||
	   (cmd->arg(2).toInt()<1)||(cmd->arg(2).toInt()>vguest_outputs)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  return;
	}
	if((vguest_input_engine_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_input_device_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_output_engine_nums[cmd->arg(2).toInt()-1]<0)||
	   (vguest_output_device_nums[cmd->arg(2).toInt()-1]<0)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  return;
	}
	sprintf(buffer,"\x02\x08\x54%c%c%c%c%c%c%c",
		(char)vguest_output_engine_nums[cmd->arg(2).toInt()-1],
		(char)(vguest_output_device_nums[cmd->arg(2).toInt()-1]>>8),
		(char)(vguest_output_device_nums[cmd->arg(2).toInt()-1]&0xFF),
		VGUEST_DEFAULT_SURFACE_NUMBER,
		(char)vguest_input_engine_nums[cmd->arg(1).toInt()-1],
		(char)(vguest_input_device_nums[cmd->arg(1).toInt()-1]>>8),
		(char)(vguest_input_device_nums[cmd->arg(1).toInt()-1]&0xFF));
	SendCommand(buffer,10);
	cmd->acknowledge(true);
	emit rmlEcho(cmd);
	break;

      case RDMacro::GO:
	if((cmd->arg(1).toInt()<1)||(cmd->arg(1).toInt()>vguest_gpos)||
	   (cmd->arg(2).toInt()<0)||(cmd->arg(2).toInt()>1)||
	   (cmd->arg(3).toInt()<0)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  return;
	}
	if(cmd->arg(3).toInt()>0) {
	  cmd_byte=0x51;
	}
	else {
	  switch(cmd->arg(2).toInt()) {
	      case 0:
		cmd_byte=0x53;
		break;

	      case 1:
		cmd_byte=0x52;
		break;
	  }
	}
	if((vguest_relays_engine_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_relays_device_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_relays_surface_nums[cmd->arg(1).toInt()-1]<0)||
	   (vguest_relays_relay_nums[cmd->arg(1).toInt()-1]<0)) {
	  cmd->acknowledge(false);
	  emit rmlEcho(cmd);
	  return;
	}
	switch(cmd_byte) {
	    case 0x51:
	      sprintf(buffer,"\x02\x07\x51%c%c%c%c%c%c",
		      vguest_relays_engine_nums[cmd->arg(1).toInt()-1],
		      vguest_relays_device_nums[cmd->arg(1).toInt()-1]>>8,
		      vguest_relays_device_nums[cmd->arg(1).toInt()-1]&0xFF,
		      vguest_relays_surface_nums[cmd->arg(1).toInt()-1],
		      vguest_relays_relay_nums[cmd->arg(1).toInt()-1],
		      cmd->arg(3).toInt()/50);
	      SendCommand(buffer,9);
	      break;

	    case 0x52:
	    case 0x53:
	      sprintf(buffer,"\x02\x06%c%c%c%c%c%c",
		      cmd_byte,
		      vguest_relays_engine_nums[cmd->arg(1).toInt()-1],
		      vguest_relays_device_nums[cmd->arg(1).toInt()-1]>>8,
		      vguest_relays_device_nums[cmd->arg(1).toInt()-1]&0xFF,
		      vguest_relays_surface_nums[cmd->arg(1).toInt()-1],
		      vguest_relays_relay_nums[cmd->arg(1).toInt()-1]);
	      SendCommand(buffer,8);
	}
	cmd->acknowledge(true);
	emit rmlEcho(cmd);
	break;

      default:
	cmd->acknowledge(false);
	emit rmlEcho(cmd);
	break;
  }
}


void VGuest::processStatus(char *buf,int size)
{
}


void VGuest::ipConnect()
{
  vguest_socket->connectToHost(vguest_ipaddress.toString(),vguest_ipport);
}


void VGuest::connectedData()
{
  vguest_istate=0;
}


void VGuest::connectionClosedData()
{
  int interval=GetHoldoff();
  LogLine(QString().sprintf("Connection to vGuest device at %s:%d closed, attempting reconnect, holdoff = %d mS",
			    (const char *)vguest_ipaddress.toString(),
			    vguest_ipport,interval));
  vguest_reconnect_timer->start(interval,true);
}


void VGuest::readyReadData()
{
  char buffer[255];
  int n=0;

  while((n=vguest_socket->readBlock(buffer,255))>0) {
    for(int i=0;i<n;i++) {
      switch(vguest_istate) {
	  case 0:   // STX Command Start
	    if(buffer[i]==0x02) {
	      vguest_istate=1;
	    }
	    break;

	  case 1:   // Command Length
	    vguest_cmd_length=buffer[i];
	    vguest_cmd_buffer[0]=2;
	    vguest_cmd_buffer[1]=buffer[i];
	    vguest_cmd_ptr=2;
	    vguest_istate=2;
	    break;

	  case 2:   // Command Body
	    vguest_cmd_buffer[vguest_cmd_ptr++]=buffer[i];
	    if(vguest_cmd_ptr==(vguest_cmd_length+2)) {
	      DispatchCommand(vguest_cmd_buffer,vguest_cmd_length+2);
	      vguest_istate=0;
	    }
	    break;

      }
    }
  }
}


void VGuest::errorData(int err)
{
  int interval=VGUEST_RECONNECT_MIN_INTERVAL;

  switch((QSocket::Error)err) {
      case QSocket::ErrConnectionRefused:
	interval=GetHoldoff();
	LogLine(QString().sprintf(
	 "Connection to vGuest device at %s:%d refused, attempting reconnect, holdoff = %d mS",
				  (const char *)vguest_ipaddress.toString(),
				  vguest_ipport,interval));
	vguest_reconnect_timer->start(interval,true);
	break;

      case QSocket::ErrHostNotFound:
	LogLine(QString().sprintf(
	  "Error on connection to vGuest device at %s:%d: Host Not Found",
				  (const char *)vguest_ipaddress.toString(),
				  vguest_ipport));
	break;

      case QSocket::ErrSocketRead:
	LogLine(QString().sprintf(
	  "Error on connection to vGuest device at %s:%d: Socket Read Error",
				  (const char *)vguest_ipaddress.toString(),
				  vguest_ipport));
	break;
  }
}


void VGuest::SendCommand(char *str,int len)
{
  // LogLine(QString().sprintf("SENT: %s",(const char *)RenderCommand(str,len)));

  switch(vguest_porttype) {
      case RDMatrix::TtyPort:
	vguest_device->writeBlock(str,len);
	break;
	
      case RDMatrix::TcpPort:
	vguest_socket->writeBlock(str,len);
	break;
  }
}


void VGuest::DispatchCommand(char *cmd,int len)
{
  char buffer[VGUEST_MAX_COMMAND_LENGTH];
  QString str;
  int linenum;

  // LogLine(QString().sprintf("RCVD: %s",(const char *)RenderCommand(cmd,len)));

  switch(cmd[2]) {
      case 0xF9:   // Username/Password Query
	buffer[0]=0x02;
	buffer[1]=0x22;
	buffer[2]=0xF9;
	buffer[3]=VGUEST_ID_BYTE;
	sprintf(buffer+4,"%s%s",
		(const char *)vguest_username,
		(const char *)vguest_password);
	SendCommand(buffer,36);
	break;

      case 0xF0:   // Connect Status
	switch(cmd[3]) {
	    case 0x0A:  // Valid connection
	    case 0x14:
	      LogLine(QString().sprintf(
			"connection to vGuest device at %s:%d established",
			(const char *)vguest_ipaddress.toString(),
			vguest_ipport));
	      break;

	    case 0x0B:  // Invalid Username
	    case 0x15:
	      LogLine(QString().sprintf(
	      "connection to vGuest device at %s:%d refused: username invalid",
			(const char *)vguest_ipaddress.toString(),
			vguest_ipport));
	      vguest_socket->close();
	      connectionClosedData();
	      break;

	    case 0x0C:  // Invalid Password
	    case 0x16:
	      LogLine(QString().sprintf(
	      "connection to vGuest device at %s:%d refused: password invalid",
			(const char *)vguest_ipaddress.toString(),
			vguest_ipport));
	      vguest_socket->close();
	      connectionClosedData();
	      break;

	    case 0x0D:  // No vGuest Permission
	    case 0x17:
	      LogLine(QString().sprintf(
	  "connection to vGuest device at %s:%d refused: no vGuest permission",
			(const char *)vguest_ipaddress.toString(),
			vguest_ipport));
	      vguest_socket->close();
	      connectionClosedData();
	      break;

	    case 0x0E:  // No Profile
	    case 0x18:
	      LogLine(QString().sprintf(
	   "connection to vGuest device at %s:%d refused: no profile assigned",
			(const char *)vguest_ipaddress.toString(),
			vguest_ipport));
	      vguest_socket->close();
	      connectionClosedData();
	      break;
	}
	break;

      case 0x52:   // Turn On
	if((linenum=GetRelay(0xFF&cmd[3],256*(0xFF&cmd[4])+(0xFF&cmd[5]),
			     0xFF&cmd[6],0xFF&cmd[7]))>=0) {
	  emit gpiChanged(vguest_matrix,linenum,true);
	}
	else {
	  LogLine(QString().sprintf("unhandled vGuest command received: %s",
				    (const char *)RenderCommand(cmd,len)));
	}
	break;

      case 0x53:   // Turn Off
	if((linenum=GetRelay(0xFF&cmd[3],256*(0xFF&cmd[4])+(0xFF&cmd[5]),
			     0xFF&cmd[6],0xFF&cmd[7]))>=0) {
	  emit gpiChanged(vguest_matrix,linenum,false);
	}
	else {
	  LogLine(QString().sprintf("unhandled vGuest command received: %s",
				    (const char *)RenderCommand(cmd,len)));
	}
	break;

      case 0x54:   // Input Assign
	break;

      case 0x55:   // Input Mode
	break;

      case 0x56:   // Fader Level
	break;

      default:
	LogLine(QString().sprintf("unrecognized vGuest command received: %s",
				  (const char *)RenderCommand(cmd,len)));
	break;
  }
}


QString VGuest::PadString(QString str,unsigned len)
{
  QString out;
  out=str.left(len);
  while(out.length()<len) {
    out+=" ";
  }
  return out;
}


QString VGuest::RenderCommand(char *cmd,int len)
{
  QString str;

  for(int i=0;i<len;i++) {
    str+=QString().sprintf("%02X",0xFF&cmd[i]);
  }
  return str;
}


int VGuest::GetRelay(int enginenum,int devicenum,int surfacenum,int relaynum)
{
  for(unsigned i=0;i<vguest_relays_engine_nums.size();i++) {
/*
    LogLine(QString().sprintf("Checking Engine: %d | %d",vguest_relays_engine_nums[i],enginenum));
    LogLine(QString().sprintf("Checking Device: 0x%04X | 0x%04X",vguest_relays_device_nums[i],devicenum));
    LogLine(QString().sprintf("Checking Surface: 0x%04X | 0x%04X",vguest_relays_surface_nums[i],surfacenum));
    LogLine(QString().sprintf("Checking Relay: 0x%04X | 0x%04X",vguest_relays_relay_nums[i],relaynum));
*/
    if((vguest_relays_engine_nums[i]==enginenum)&&
       (vguest_relays_device_nums[i]==devicenum)&&
       (vguest_relays_surface_nums[i]==surfacenum)&&
       (vguest_relays_relay_nums[i]==relaynum)) {
//      LogLine("  MATCH!");
      return (int)i;
    }
  }
  return -1;
}


int VGuest::GetHoldoff()
{
  return (int)(VGUEST_RECONNECT_MIN_INTERVAL+
	       (VGUEST_RECONNECT_MAX_INTERVAL-VGUEST_RECONNECT_MIN_INTERVAL)*
	       (double)random()/(double)RAND_MAX);
}
